d5e8ae58a36c579302ee6daff575c0cb430c4dff
[nextcloud-desktop.git] /
1 //
2 //  ShareTableViewDataSource.swift
3 //  FileProviderUIExt
4 //
5 //  Created by Claudio Cambra on 27/2/24.
6 //
7
8 import AppKit
9 import FileProvider
10 import NextcloudKit
11 import NextcloudFileProviderKit
12 import NextcloudCapabilitiesKit
13 import OSLog
14
15 class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
16     private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
17     private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
18     private let reattemptInterval: TimeInterval = 3.0
19
20     let kit = NextcloudKit.shared
21
22     var uiDelegate: ShareViewDataSourceUIDelegate?
23     var sharesTableView: NSTableView? {
24         didSet {
25             sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
26             sharesTableView?.rowHeight = 42.0  // Height of view in ShareTableItemView XIB
27             sharesTableView?.dataSource = self
28             sharesTableView?.delegate = self
29             sharesTableView?.reloadData()
30         }
31     }
32     var capabilities: Capabilities?
33
34     private(set) var itemURL: URL?
35     private(set) var itemServerRelativePath: String?
36     private(set) var shares: [NKShare] = [] {
37         didSet { Task { @MainActor in sharesTableView?.reloadData() } }
38     }
39     private(set) var account: Account? {
40         didSet {
41             guard let account = account else { return }
42             kit.appendSession(
43                 account: account.ncKitAccount,
44                 urlBase: account.serverUrl,
45                 user: account.username,
46                 userId: account.username,
47                 password: account.password,
48                 userAgent: "Nextcloud-macOS/FileProviderUIExt",
49                 nextcloudVersion: 25,
50                 groupIdentifier: ""
51             )
52         }
53     }
54
55     func loadItem(url: URL) {
56         itemServerRelativePath = nil
57         itemURL = url
58         Task {
59             await reload()
60         }
61     }
62
63     func reattempt() {
64         DispatchQueue.main.async {
65             Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
66                 Task { await self.reload() }
67             }
68         }
69     }
70
71     func reload() async {
72         guard let itemURL else {
73             presentError("No item URL, cannot reload data!")
74             return
75         }
76         guard let itemIdentifier = await withCheckedContinuation({
77             (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
78             NSFileProviderManager.getIdentifierForUserVisibleFile(
79                 at: itemURL
80             ) { identifier, domainIdentifier, error in
81                 defer { continuation.resume(returning: identifier) }
82                 guard error == nil else {
83                     self.presentError("No item with identifier: \(error.debugDescription)")
84                     return
85                 }
86             }
87         }) else {
88             presentError("Could not get identifier for item, no shares can be acquired.")
89             return
90         }
91
92         do {
93             let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
94                 Logger.sharesDataSource.error("Service connection interrupted")
95             })
96             guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
97                   let credentials = await connection.credentials() as? Dictionary<String, String>,
98                   let convertedAccount = Account(dictionary: credentials),
99                   !convertedAccount.password.isEmpty
100             else {
101                 presentError("Failed to get details from File Provider Extension. Retrying.")
102                 reattempt()
103                 return
104             }
105             let serverPathString = serverPath as String
106             itemServerRelativePath = serverPathString
107             account = convertedAccount
108             await sharesTableView?.deselectAll(self)
109             capabilities = await fetchCapabilities()
110             guard capabilities != nil else { return }
111             guard capabilities?.filesSharing?.apiEnabled == true else {
112                 presentError("Server does not support shares.")
113                 return
114             }
115             guard let account else {
116                 presentError("Account data is unavailable, cannot reload data!")
117                 return
118             }
119             guard let itemMetadata = await fetchItemMetadata(
120                 itemRelativePath: serverPathString, account: account, kit: kit
121             ) else {
122                 presentError("Unable to retrieve file metadata...")
123                 return
124             }
125             guard itemMetadata.permissions.contains("R") == true else {
126                 presentError("This file cannot be shared.")
127                 return
128             }
129             shares = await fetch(
130                 itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
131             )
132         } catch let error {
133             presentError("Could not reload data: \(error), will try again.")
134             reattempt()
135         }
136     }
137
138     private func fetch(
139         itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
140     ) async -> [NKShare] {
141         Task { @MainActor in uiDelegate?.fetchStarted() }
142         defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
143
144         let rawIdentifier = itemIdentifier.rawValue
145         Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
146
147         guard let account else {
148             self.presentError("NextcloudKit instance or account is unavailable, cannot fetch shares!")
149             return []
150         }
151
152         let parameter = NKShareParameter(path: itemRelativePath)
153
154         return await withCheckedContinuation { continuation in
155             kit.readShares(
156                 parameters: parameter, account: account.ncKitAccount
157             ) { account, shares, data, error in
158                 let shareCount = shares?.count ?? 0
159                 Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
160                 defer { continuation.resume(returning: shares ?? []) }
161                 guard error == .success else {
162                     self.presentError("Error fetching shares: \(error.errorDescription)")
163                     return
164                 }
165             }
166         }
167     }
168
169     private static func generateInternalShare(for file: NKFile) -> NKShare {
170         let internalShare = NKShare()
171         internalShare.shareType = NKShare.ShareType.internalLink.rawValue
172         internalShare.url = file.urlBase +  "/index.php/f/" + file.fileId
173         internalShare.account = file.account
174         internalShare.displaynameOwner = file.ownerDisplayName
175         internalShare.displaynameFileOwner = file.ownerDisplayName
176         internalShare.path = file.path
177         return internalShare
178     }
179
180     private func fetchCapabilities() async -> Capabilities? {
181         guard let account else {
182             self.presentError("Could not fetch capabilities as account is invalid.")
183             return nil
184         }
185
186         return await withCheckedContinuation { continuation in
187             kit.getCapabilities(account: account.ncKitAccount) { account, data, error in
188                 guard error == .success, let capabilitiesJson = data?.data else {
189                     self.presentError("Error getting server caps: \(error.errorDescription)")
190                     continuation.resume(returning: nil)
191                     return
192                 }
193                 Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
194                 continuation.resume(returning: Capabilities(data: capabilitiesJson))
195             }
196         }
197     }
198
199     private func presentError(_ errorString: String) {
200         Logger.sharesDataSource.error("\(errorString, privacy: .public)")
201         Task { @MainActor in self.uiDelegate?.showError(errorString) }
202     }
203
204     // MARK: - NSTableViewDataSource protocol methods
205
206     @objc func numberOfRows(in tableView: NSTableView) -> Int {
207         shares.count
208     }
209
210     // MARK: - NSTableViewDelegate protocol methods
211
212     @objc func tableView(
213         _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
214     ) -> NSView? {
215         let share = shares[row]
216         guard let view = tableView.makeView(
217             withIdentifier: shareItemViewIdentifier, owner: self
218         ) as? ShareTableItemView else {
219             Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
220             return nil
221         }
222         view.share = share
223         return view
224     }
225
226     @objc func tableViewSelectionDidChange(_ notification: Notification) {
227         guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
228             Task { @MainActor in uiDelegate?.hideOptions(self) }
229             return
230         }
231         let share = shares[selectedRow]
232         Task { @MainActor in uiDelegate?.showOptions(share: share) }
233     }
234 }